 /*******************************************************************************
  * Copyright (c) 2004, 2005 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.core.internal.localstore;

 import java.io.*;
 import org.eclipse.core.internal.localstore.Bucket.Visitor;
 import org.eclipse.core.internal.resources.ResourceException;
 import org.eclipse.core.internal.resources.Workspace;
 import org.eclipse.core.internal.utils.Messages;
 import org.eclipse.core.resources.IProject;
 import org.eclipse.core.resources.IResourceStatus;
 import org.eclipse.core.runtime.*;
 import org.eclipse.osgi.util.NLS;


 /**
  * @since 3,1
  */
 public class BucketTree {
     public static final int DEPTH_INFINITE = Integer.MAX_VALUE;
     public static final int DEPTH_ONE = 1;
     public static final int DEPTH_ZERO = 0;

     private final static int SEGMENT_QUOTA = 256; //two hex characters

     /**
      * Store all bucket names to avoid creating garbage when traversing the tree
      */
     private static final char[][] HEX_STRINGS;
     
     static {
         HEX_STRINGS = new char[SEGMENT_QUOTA][];
         for (int i = 0; i < HEX_STRINGS.length; i++)
             HEX_STRINGS[i] = Integer.toHexString(i).toCharArray();
     }

     protected Bucket current;

     private Workspace workspace;

     public BucketTree(Workspace workspace, Bucket bucket) {
         this.current = bucket;
         this.workspace = workspace;
     }

     /**
      * From a starting point in the tree, visit all nodes under it.
      * @param visitor
      * @param base
      * @param depth
      */
     public void accept(Bucket.Visitor visitor, IPath base, int depth) throws CoreException {
         if (Path.ROOT.equals(base)) {
             current.load(null, locationFor(Path.ROOT));
             if (current.accept(visitor, base, DEPTH_ZERO) != Visitor.CONTINUE)
                 return;
             if (depth == DEPTH_ZERO)
                 return;
             boolean keepVisiting = true;
             depth--;
             IProject[] projects = workspace.getRoot().getProjects();
             for (int i = 0; keepVisiting && i < projects.length; i++) {
                 IPath projectPath = projects[i].getFullPath();
                 keepVisiting = internalAccept(visitor, projectPath, locationFor(projectPath), depth, 1);
             }
         } else
             internalAccept(visitor, base, locationFor(base), depth, 0);
     }

     public void close() throws CoreException {
         current.save();
         saveVersion();
     }

     public Bucket getCurrent() {
         return current;
     }

     public File getVersionFile() {
         return new File(locationFor(Path.ROOT), current.getVersionFileName());
     }

     /**
      * This will never be called for a bucket for the workspace root.
      *
      * @return whether to continue visiting other branches
      */
     private boolean internalAccept(Bucket.Visitor visitor, IPath base, File bucketDir, int depthRequested, int currentDepth) throws CoreException {
         current.load(base.segment(0), bucketDir);
         int outcome = current.accept(visitor, base, depthRequested);
         if (outcome != Visitor.CONTINUE)
             return outcome == Visitor.RETURN;
         if (depthRequested <= currentDepth)
             return true;
         File[] subDirs = bucketDir.listFiles();
         if (subDirs == null)
             return true;
         for (int i = 0; i < subDirs.length; i++)
             if (subDirs[i].isDirectory())
                 if (!internalAccept(visitor, base, subDirs[i], depthRequested, currentDepth + 1))
                     return false;
         return true;
     }

     public void loadBucketFor(IPath path) throws CoreException {
         current.load(Path.ROOT.equals(path) ? null : path.segment(0), locationFor(path));
     }

     private File locationFor(IPath resourcePath) {
         //optimized to avoid string and path creations
 IPath baseLocation = workspace.getMetaArea().locationFor(resourcePath).removeTrailingSeparator();
         int segmentCount = resourcePath.segmentCount();
         String locationString = baseLocation.toOSString();
         StringBuffer locationBuffer = new StringBuffer (locationString.length() + Bucket.INDEXES_DIR_NAME.length() + 16);
         locationBuffer.append(locationString);
         locationBuffer.append(File.separatorChar);
         locationBuffer.append(Bucket.INDEXES_DIR_NAME);
         // the last segment is ignored
 for (int i = 1; i < segmentCount - 1; i++) {
             // translate all segments except the first one (project name)
 locationBuffer.append(File.separatorChar);
             locationBuffer.append(translateSegment(resourcePath.segment(i)));
         }
         return new File(locationBuffer.toString());
     }

     /**
      * Writes the version tag to a file on disk.
      */
     private void saveVersion() throws CoreException {
         File versionFile = getVersionFile();
         if (!versionFile.getParentFile().exists())
             versionFile.getParentFile().mkdirs();
         FileOutputStream stream = null;
         boolean failed = false;
         try {
             stream = new FileOutputStream(versionFile);
             stream.write(current.getVersion());
         } catch (IOException e) {
             failed = true;
             String message = NLS.bind(Messages.resources_writeWorkspaceMeta, versionFile.getAbsolutePath());
             throw new ResourceException(IResourceStatus.FAILED_WRITE_METADATA, null, message, e);
         } finally {
             try {
                 if (stream != null)
                     stream.close();
             } catch (IOException e) {
                 if (!failed) {
                     String message = NLS.bind(Messages.resources_writeWorkspaceMeta, versionFile.getAbsolutePath());
                     throw new ResourceException(IResourceStatus.FAILED_WRITE_METADATA, null, message, e);
                 }
             }
         }
     }

     private char[] translateSegment(String segment) {
         // String.hashCode algorithm is API
 return HEX_STRINGS[Math.abs(segment.hashCode()) % SEGMENT_QUOTA];
     }
 }

